04_DDD/05 -- CQRS y Proyecciones.md

CQRS y Proyecciones

Los agregados están diseñados para escritura: garantizan invariantes, gestionan transacciones, registran eventos. Pero no son necesariamente la representación óptima para leer datos.

Cuando el módulo de descripciones necesita mostrar una lista de descripciones con el nombre legible de cada recurso, necesita combinar datos de múltiples tablas. Sin BCs, haría JOINs entre módulos. Con BCs, no puede hacer esos JOINs. ¿Cómo resuelve el conflicto?

La respuesta es CQRS (Command Query Responsibility Segregation): usar modelos distintos para escritura y para lectura.


CQRS: separar escritura y lectura

La idea central de CQRS es sencilla: el modelo que garantiza la consistencia en escritura (el agregado) no tiene por qué ser el modelo que se usa para leer. Se pueden tener dos representaciones distintas del mismo concepto:

ModeloPropósitoCaracterísticas
Write ModelGarantizar invariantes, registrar eventosTiene lógica de negocio, reglas, validaciones
Read ModelDevolver datos al frontend con eficienciaCasi un DTO, sin lógica, optimizado para lectura

El Write Model es la fuente de verdad. El Read Model es una proyección — una vista desnormalizada calculada a partir de los eventos del Write Model.


Proyecciones: vistas materializadas gestionadas por la aplicación

Una proyección es una vista materializada cuya lógica de materialización recae sobre la aplicación, no sobre la base de datos.

El flujo de una proyección event-driven:

Write Model (módulo de variables)
  │
  │ 1. VariableCreated { id: 42, name: "T_HORNO_PRINCIPAL" }
  │
  ▼
EventBus
  │
  │ 2. Listener escucha el evento
  │
  ▼
Read Model (módulo de descripciones)
  │
  │ 3. INSERT INTO module_description_variable (id=42, name='T_HORNO_PRINCIPAL')
  │
  ▼
module_description_variable (tabla mirror = proyección)

El cálculo pesado ocurre en el momento del INSERT (escritura), no del SELECT (lectura). El GET posterior es un SELECT * FROM module_description_variable WHERE id = ? sin JOINs.

El patrón mirror del módulo de descripciones (presentado en el capítulo 3) es exactamente una proyección event-driven. Cada tabla mirror es el Read Model del módulo de descripciones sobre un recurso de otro BC:

Tabla mirrorBC de origenEvento que la actualiza
module_description_variableMódulo de variablesVariableCreated, VariableChanged, VariableRemoved
module_description_groupMódulo de gruposGroupCreated, GroupChanged, GroupRemoved
module_description_userMódulo de usuariosUserCreated, UserChanged, UserRemoved
.........

Cada tabla contiene solo {id, name} — exactamente lo que el módulo de descripciones necesita del recurso externo para funcionar de forma autónoma. Nada más.


Las cuatro alternativas para evitar JOINs

Sin embargo las proyecciones no son la única solución al problema de los JOINs entre módulos. Hay cuatro enfoques, todos ellos presentes dentro de DWall

AlternativaDescripciónCuándo usarla
Repository como anticorrupciónEl repositorio ejecuta JOINs internamente, ocultos al dominioInicio del proyecto, datos poco voluminosos
Vistas lógicas de BDCREATE VIEW que absorbe los JOINsDominio simple, rendimiento no crítico
Vistas materializadasPre-computan los JOINs en BDCuando la BD (PostgreSQL) puede gestionar el refresh
Proyecciones event-drivenLa aplicación mantiene el Read ModelAlta carga de lectura, BCs bien definidos

DWall usa proyecciones event-driven para los mirrors del módulo de descripciones. La elección es coherente con la arquitectura de BCs: evitas JOINs cruzados entre módulos y tampoco requieres la infraestructura de vistas materializadas.

Sin embargo, tal como veremos en el capítulo de la base de datos vectorial, nada de esto es blanco y negro. La elección puede depender del volumen de datos, del coste de mantener los listeners y del grado de consistencia requerido — y existen casos en las opciones menos puristas resultan más convenientes.


Read Model: casi un DTO

El objeto que devuelve el módulo de descripciones para el frontend — el NamedDescription con descripción, nombre del recurso, tipo, usuario y timestamp — es prácticamente un DTO. No tiene lógica de negocio. No registra eventos. No garantiza invariantes.

Esto es correcto. Un Read Model no necesita ser un agregado. Su único trabajo es estar disponible para ser leído eficientemente.

Write: Description.create(...) → guarda en module_description_description
                                → registra DescriptionCreated
                                
Read:  NamedDescription         → join entre description + mirror (solo id+name)
                                → nada de eventos, nada de lógica

La distinción no requiere carpetas separadas read-model/ y write-model/. Se entiende de forma inherente: la fuente de verdad (descripción principal) es el Write Model; las tablas mirror que sirven al frontend son el Read Model.